#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<assert.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/wait.h>

//
#define LEFT "["
#define RIGHT "]"
#define SIGNCH "$"
#define LINE_SIZE 1024 //作为一个行大小的标志
#define ARGC_SIZE 24 // 命令行支持的最大参数个数
#define DELIM " \t" //分割的字符集合
#define EXIT_CODE 44
//
int quit = 0; //标志退出当前shell
char commandline[LINE_SIZE] = {'\0'}; //获取命令行的输入，存储在里面
char* argv[ARGC_SIZE] = {NULL}; //用于存放解析的指令参数 -- 不能是const char* const，需要修改其中的内容和指向。
int lastcode = 0; //获取子进程的退出码
char pwd[LINE_SIZE] = {'\0'}; //维护当前工作路径
char myenv[LINE_SIZE] = {'\0'}; //维护自己定义的环境变量
//
const char* getuser()//返回用户名
{
	return getenv("USER");
}
const char* _gethostname() //返回主机名 -- 命名冲突了
{
	return "hcss-ecs-ca4a"; //环境变量中没有主机名：hostname
}

//由于内建命令cd的需要，我们需要将其改变
//const char* getpwd() //返回路径
//{
//	return getenv("PWD");
//}

void getpwd() //不再去改变环境变量了
{
	getcwd(pwd, sizeof(pwd));
}

//实现用户交互
void interact()
{
	getpwd(); //更新当前工作路径
	printf(LEFT"%s@%s:%s"RIGHT""SIGNCH" ",getuser(), _gethostname(), pwd); //命令行提示
	char *str = fgets(commandline, sizeof(commandline), stdin); //从标准输入流中获取输入
	assert(str); //这个输入一定不为NULL
	
	//由于fgets是行获取，所以\n也在这个数组中，下面进行特殊处理：
	commandline[strlen(commandline)-1] = '\0'; // "aabbc\n\0" --> '\n'对应下标为长度-1
	//printf("%s\n", commandline);	
}

int splitstr(char* cline, char* _argv[])
{
	int i = 0;
	_argv[i++] = strtok(cline, DELIM);
	while(_argv[i++] = strtok(NULL, DELIM)); //使用赋值运算符结束返回NULL，条件判断为NULL，结束！
	_argv[i] = NULL; //保证最后一个参数的下一个位置是NULL指针！
	
	return i - 1; //返回当前参数个数
}

void normalexe(char* _argv[])
{
	pid_t id = fork();
	if(id < 0)
	{
		perror("fork"); //子进程创建失败
		return;
	}
	else if(id == 0)
	{	//调用子进程
		execvp(_argv[0], _argv);
		exit(EXIT_CODE); //子进程调用失败，假设退出码为44;
	}
	else
	{
		//父进程进行等待
		int status = 0;
		pid_t ret = waitpid(id, &status, 0); //以阻塞方式等待
		if(ret == id)
		{
			//子进程正常退出 -- 得到退出码发现指令执行的正确性
			lastcode = WEXITSTATUS(status);
			//if(lastcode != 0)
				//printf("_argv[0]:%s", strerror(lastcode));
			
		}
		else{//子进程异常退出
		}
	}	
}


int buildexe(int _argc, char* _argv[]) //通过返回值判断是否为内建命令
{
	//执行内建命令
	if(_argc == 2 && strcmp(_argv[0],"cd") == 0) //cd命令
	{
		chdir(_argv[1]); //改变到指定路径 由于myshell执行了cd命令过后，系统的环节变量并不改变所以需要特殊处理
		//方案一：
		//sprintf(getenv("PWD"), "%s", _argv[1]);
		//缺陷：输入一个不存在的路径的时候，环节变量会发生改变，但是工作路径不会发生改变！所以需要一个麻烦的特殊判断
		//方案二：
		//利用接口，直接获取当前工作路径：getcwd
		getpwd();
		lastcode = 0;
		return 0;
	}
	else if(_argc == 2 && strcmp(_argv[0], "echo") == 0) //echo的内建命令
	{
		if(strcmp(_argv[1], "$?") == 0)
		{
			printf("%d\n", lastcode);
		}
		else if(_argv[1][0] == '$')
		{
			char* val = getenv(_argv[1] + 1);
			if(val) printf("%s\n", val);
		}
		else
		{
			printf("%s\n", _argv[1]);
		}
		lastcode = 0;
		return 0;
	}
	else if(_argc == 2 && strcmp(_argv[0], "export") == 0) //export的内建命令
	{
		//由于，putenv的函数，只会把字符串的首地址，拷到环境变量表中，而不能直接添加到环境变量存储区域，所以需要自己维护一段堆/全局空间来维护自己导入的环境变量
		//如果不采用堆/全局空间，采用栈空间，那么每次输入的时候都会覆盖原来的空间，导致环境变量无法保存下来
		strcpy(myenv, _argv[1]);
		putenv(myenv);
		lastcode = 0;
		return 0;
	}
	return 1;
}

int main()
{
	while(!quit)
	{
		//2.实现bash与用户的交互：
		interact();

		//3.解析字符串
		int argc = splitstr(commandline, argv);	
		
		//4.指令的判断
		if(!strcmp("quit", argv[0])) break; //输入quit就停止
		else if(argc == 0) continue; //如果直接换行输入就下一次
		//进行内建命令的判断
		int flag = buildexe(argc, argv); //返回代表不是内建命令

		//5.普通指令的执行
		if(flag) normalexe(argv);
	}

	
	return 0;
}
